﻿//-------------------------------------------------------------------------------------
// <copyright file="Fragment.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation. All rights reserved.
//      THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
//      EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
//      WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
// </copyright>
// <author>Mark Phillips</author>
// <email>v-mphill@microsoft.com</email>
// <date>2004-08-19</date>
// <summary>
//      Allows the creation of a WiX based xml fragment containing a nested list of
//      directories and components.  The directories and files are built from the supplied
//      list of files.  Guids are supplied for the components to ensure uniqueness.  The resulting
//      Xml fragment is returned as an output parameter, plus an item list of all the components
//      created.
// </summary>  
//-------------------------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.Wix
{
  #region Using directives

  using System;
  using System.Text;
  using System.IO;
  using System.Collections;
  using System.Collections.Specialized;
  using System.Xml;
  using System.Runtime.InteropServices;
  using Microsoft.Build.Framework;
  #endregion

  /// <summary>
  /// Base class for tasks relating to <b>Wix</b>, the Microsoft tool to build MSI packages from XML 
  /// source files.
  /// </summary>
  /// <remarks>
  /// <para>
  /// <b>About Wix:</b> Wix was originally a Microsoft tool that has been released into the open source
  /// community under the Common Public License. The binaries and source code can be downloaded from
  /// <a href="http://sourceforge.net/projects/wix">SourceForge</a>.
  /// </para>
  /// </remarks>
  public class Fragment : TaskBase
  {
    #region Member Variables

    private string directoryRef = String.Empty;
    private string rootPath = String.Empty;
    private string outputPath = String.Empty;
    private string outputXml = String.Empty;
    private string assemblyType;
    private string[] sourceFiles = null;
    private string[] components = null;
    private StringCollection comp = null;
    private bool wrapEachFile = false;

    #endregion

    #region Properties

    [Required]
    public string DirectoryRef
    {
      get
      {
        return directoryRef;
      }
      set
      {
        directoryRef = value;
      }
    }

    [Required]
    public string RootPath
    {
      get
      {
        return rootPath;
      }

      set
      {
        rootPath = value;
      }
    }

    public string OutputPath
    {
      get
      {
        return outputPath;
      }

      set
      {
        outputPath = value;
      }
    }

    [Required]
    public string[] SourceFiles
    {
      get
      {
        return sourceFiles;
      }

      set
      {
        sourceFiles = value;
      }
    }

    /// <summary>
    /// Should each file be wrapped in its own component - handy when wrapping assemblies so that the the compoenent 
    /// rules are obeyed http://msdn.microsoft.com/library/default.asp?url=/library/en-us/msi/setup/organizing_applications_into_components.asp
    /// </summary>
    public bool WrapEachFileInAComponent
    {
      get
      {
        return wrapEachFile;
      }

      set
      {
        wrapEachFile = value;
      }
    }

    [Output]
    public string[] Components
    {
      get
      {
        return components;
      }

      set
      {
        components = value;
      }
    }

    [Output]
    public string OutputXml
    {
      get
      {
        return (this.outputXml ?? String.Empty);
      }
      set
      {
        this.outputXml = value;
      }
    }

    /// <summary>
    /// AssemblyType. Current valid values are: no, win32, .net
    /// If .net is specified, WrapEachFileInAComponent should be set to true.
    /// </summary>
    public string AssemblyType
    {
        get { return this.assemblyType; }
        set { this.assemblyType = value; }
    }

    #endregion

    #region Public Members

    protected override void InternalExecute()
    {
      if (directoryRef == null || directoryRef.Length == 0)
      {
        throw new ArgumentException("Property DirectoryRef must contain a value.");
      }

      if (this.sourceFiles == null || this.sourceFiles.Length == 0)
      {
        throw new ArgumentException("Property SourceFiles must contain at least one value.");
      }

      if (this.rootPath == null || this.rootPath == String.Empty || this.rootPath.Length == 0)
      {
        throw new ArgumentException("Property RootPath must contain a valid directory");
      }

      if (!Directory.Exists(this.rootPath))
      {
        throw new ArgumentException("Property RootPath must contain a valid directory");
      }

      string[] sourceFileList;

      comp = new StringCollection();

      Hashtable dirs = new Hashtable();

      if (sourceFiles != null && sourceFiles.Length > 0)
      {
        foreach (string sourceFileMask in sourceFiles)
        {

          string directoryName = Path.GetDirectoryName(sourceFileMask);

          if (!directoryName.StartsWith(rootPath))
          {
            throw new Exception(String.Format("The file {0} is not a subdirectory of {1}.", sourceFileMask, rootPath));
          }

          string fileMask = Path.GetFileName(sourceFileMask);

          StringCollection files;
          if (dirs.ContainsKey(directoryName))
          {
            files = (StringCollection)dirs[directoryName];
          }
          else
          {
            files = new StringCollection();
            dirs.Add(directoryName, files);
          }

          sourceFileList = Directory.GetFiles(directoryName, fileMask);

          if (sourceFileList != null && sourceFileList.Length > 0)
          {
            foreach (string sourceFile in sourceFileList)
            {
              string filename = Path.GetFileName(sourceFile);
              if (!files.Contains(filename))
              {
                files.Add(filename);
              }
            }

            dirs[directoryName] = files;
          }
        }

        IDictionaryEnumerator e = dirs.GetEnumerator();

        XmlDocument doc = new XmlDocument();
        XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "utf-8", null);

        XmlElement root = doc.CreateElement("Wix", "http://schemas.microsoft.com/wix/2003/01/wi");
        doc.InsertBefore(xmlDeclaration, doc.DocumentElement);
        doc.AppendChild(root);

        XmlElement frag = doc.CreateElement("Fragment", "http://schemas.microsoft.com/wix/2003/01/wi");
        root.AppendChild(frag);

        XmlElement refElem = doc.CreateElement("DirectoryRef", "http://schemas.microsoft.com/wix/2003/01/wi");
        XmlAttribute refAttr = doc.CreateAttribute("Id");
        refAttr.Value = this.directoryRef;
        refElem.Attributes.Append(refAttr);

        frag.AppendChild(refElem);

        // Create the directories
        while (e.MoveNext())
        {
          string dirName = (string)e.Key;
          if (dirName.Length < rootPath.Length)
          {
            throw new Exception(String.Format("Directory {0} is not a sub-directory of, or the same as, the root directory {1}.", dirName, rootPath));
          }

          string dirNameLessRoot = "";

          if (dirName.Length > rootPath.Length)
          {
            dirNameLessRoot = dirName.Substring(rootPath.Length + 1);
          }

          CreatePath(dirNameLessRoot, doc, refElem, rootPath, dirs);
        }

        this.outputXml = doc.OuterXml;

        // Output the fragment to the console or a file if specified.
        if (this.outputPath != null && this.outputPath.Length > 0)
        {
          doc.Save(this.outputPath);
        }
        else
        {
          doc.Save(Console.Out);
        }

        // Create the list of component Guids that have been generated.
        if (comp.Count > 0)
        {
          components = new string[comp.Count];
          int index = 0;
          foreach (string c in comp)
          {
            components[index] = c;
            index++;
          }
        }
      }
    }

    #endregion

    #region Private Members

    private void CreatePath(string path, XmlDocument doc, XmlElement currentNode, string fullPath, Hashtable dirs)
    {
      string[] dirParts = path.Split(new char[] { '\\' });
      XmlElement elem;

      string newFullPath = fullPath;

      if (dirParts[0] != null && dirParts[0].Length > 0)
      {
        newFullPath += "\\" + dirParts[0];
      }

      DirectoryInfo directory = new DirectoryInfo(newFullPath);
      if (!directory.Exists)
      {
        throw new Exception(String.Format("Directory {0} does not exist.", newFullPath));
      }

      string dirName = directory.Name;
      string dirShortName = Path.GetFileName(GetShortPathName(directory.FullName));

      if (currentNode != null)
      {
        if (currentNode.ChildNodes == null || currentNode.ChildNodes.Count == 0)
        {
          if (dirParts[0] != null && dirParts[0].Length > 0)
          {
            elem = doc.CreateElement("Directory", "http://schemas.microsoft.com/wix/2003/01/wi");
            XmlAttribute attr = doc.CreateAttribute("Id");
            attr.Value = "_" + Guid.NewGuid().ToString().Replace('-', '_');
            XmlAttribute attr2 = doc.CreateAttribute("Name");
            attr2.Value = dirShortName;
            elem.Attributes.Append(attr);
            elem.Attributes.Append(attr2);

            if (dirShortName != dirName)
            {
              XmlAttribute attr3 = doc.CreateAttribute("LongName");
              attr3.Value = dirName;
              elem.Attributes.Append(attr3);
            }

            currentNode.AppendChild(elem);
          }
          else
          {
            elem = currentNode;
          }
          CreateFiles(doc, elem, dirs, newFullPath);
        }
        else
        {
          IEnumerator e = currentNode.ChildNodes.GetEnumerator();
          elem = null;
          XmlElement tmpElem;
          while (e.MoveNext())
          {
            tmpElem = (XmlElement)e.Current;
            if (tmpElem.Name == "Directory")
            {
              XmlAttribute attr = (XmlAttribute)tmpElem.Attributes.GetNamedItem("Name");
              XmlAttribute attr2 = (XmlAttribute)tmpElem.Attributes.GetNamedItem("LongName");
              if (attr.Value == dirParts[0] || (attr2 != null && attr2.Value == dirParts[0]))
              {
                elem = tmpElem;
                break;
              }
            }
          }

          if (elem == null)
          {
            if (dirParts[0] != null && dirParts[0].Length > 0)
            {
              elem = doc.CreateElement("Directory", "http://schemas.microsoft.com/wix/2003/01/wi");
              XmlAttribute attr = doc.CreateAttribute("Id");
              attr.Value = "_" + Guid.NewGuid().ToString().Replace('-', '_');
              XmlAttribute attr2 = doc.CreateAttribute("Name");
              attr2.Value = dirShortName;
              elem.Attributes.Append(attr);
              elem.Attributes.Append(attr2);

              if (dirShortName != dirName)
              {
                XmlAttribute attr3 = doc.CreateAttribute("LongName");
                attr3.Value = dirName;
                elem.Attributes.Append(attr3);
              }

              currentNode.AppendChild(elem);
            }
            else
            {
              elem = currentNode;
            }

            CreateFiles(doc, elem, dirs, newFullPath);
          }
        }

        if (dirParts.Length > 1)
        {
          CreatePath(path.Substring(dirParts[0].Length + 1), doc, elem, newFullPath, dirs);
        }
      }
      else
      {
        throw new Exception("Null node");
      }

    }

    private void CreateFiles(XmlDocument doc, XmlElement currentNode, Hashtable dirs, string directoryName)
    {
      StringCollection files = (StringCollection)dirs[(string)directoryName];

      if (files != null && files.Count > 0)
      {
        XmlElement elem = null;

        if (this.wrapEachFile == false)
        {
          elem = CreateComponentNode(doc);
        }

        foreach (string file in files)
        {
          if (this.wrapEachFile)
          {
            elem = CreateComponentNode(doc);
          }

          string fileName = Path.GetFileName(Path.Combine(directoryName, file));
          string shortFileName = Path.GetFileName(GetShortPathName(Path.GetFullPath(Path.Combine(directoryName, file))));

          XmlElement fileElem = doc.CreateElement("File", "http://schemas.microsoft.com/wix/2003/01/wi");
          XmlAttribute fileIdAttr = doc.CreateAttribute("Id");
          fileIdAttr.Value = "_" + Guid.NewGuid().ToString().Replace('-', '_');
          XmlAttribute fileNameAttr = doc.CreateAttribute("Name");
          fileNameAttr.Value = shortFileName;
          fileElem.Attributes.Append(fileIdAttr);
          fileElem.Attributes.Append(fileNameAttr);

          if (shortFileName != fileName)
          {
            XmlAttribute fileLongNameAttr = doc.CreateAttribute("LongName");
            fileLongNameAttr.Value = fileName;
            fileElem.Attributes.Append(fileLongNameAttr);
          }

          XmlAttribute fileVitalAttr = doc.CreateAttribute("Vital");
          fileVitalAttr.Value = "yes";
          fileElem.Attributes.Append(fileVitalAttr);

          XmlAttribute fileSrcAttr = doc.CreateAttribute("src");
          fileSrcAttr.Value = Path.Combine(directoryName, file);
          fileElem.Attributes.Append(fileSrcAttr);

          XmlAttribute fileDiskAttr = doc.CreateAttribute("DiskId");
          fileDiskAttr.Value = "1";
          fileElem.Attributes.Append(fileDiskAttr);

          // Allow Fragment generation with assembly type specified
          if (!string.IsNullOrEmpty(assemblyType))
          {
              XmlAttribute assemblyAttr = doc.CreateAttribute("Assembly");
              assemblyAttr.Value = assemblyType;
              fileElem.Attributes.Append(assemblyAttr);

              // If Assembly is specified, KeyPath must also be specified.              
              XmlAttribute keyPathAttr = doc.CreateAttribute("KeyPath");
              keyPathAttr.Value = "yes";
              fileElem.Attributes.Append(keyPathAttr);
          }

          elem.AppendChild(fileElem);

          if (this.wrapEachFile)
          {
            currentNode.AppendChild(elem);
          }
        }

        if (this.wrapEachFile == false)
        {
          currentNode.AppendChild(elem);
        }
      }
    }

    private XmlElement CreateComponentNode(XmlDocument doc)
    {
      XmlElement elem;
      elem = doc.CreateElement("Component", "http://schemas.microsoft.com/wix/2003/01/wi");
      XmlAttribute attrId = doc.CreateAttribute("Id");
      Guid compGuid = Guid.NewGuid();
      string compId = "_" + compGuid.ToString().Replace('-', '_');
      attrId.Value = compId;
      comp.Add(compId);
      XmlAttribute attrGuid = doc.CreateAttribute("Guid");
      attrGuid.Value = compGuid.ToString();

      elem.Attributes.Append(attrId);
      elem.Attributes.Append(attrGuid);
      return elem;
    }

    static string GetShortPathName(string fullPath)
    {
      StringBuilder shortPath = new StringBuilder(255, 255);

      GetShortPathName(fullPath, shortPath, 255);
      return shortPath.ToString();
    }

    [DllImport("kernel32.dll", EntryPoint = "GetShortPathNameW", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
    internal static extern uint GetShortPathName(string longPath, StringBuilder shortPath, [MarshalAs(UnmanagedType.U4)]int buffer);

    #endregion


  }
}